package edu.pitt.myptm.datamanager;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Hashtable;

import edu.pitt.myptm.DataItem;
import edu.pitt.myptm.MyCommand;
import edu.pitt.myptm.utils.DBUtils;

public class BufferManager {
	private int pageNum;// The maximum num of buffer pages
	byte[] buffer;
	private Hashtable<String, MyFileOperator> operatorList = new Hashtable<String, MyFileOperator>();

	private int pageLimit;// The maximum num of pages of a harddisk file

	private int pageCursor = 0;

	private Hashtable<String, Integer> pageBind; // Given the block name and the
	// block page, we can go the
	// specified page in buffer
	private ArrayList<PageInfo> bmt; // Given the page number, we can
	// track the information of the
	// page, and its position on
	// disk

	private ArrayList<Integer> unfixedPages = new ArrayList<Integer>();
	private ArrayList<Integer> emptyPages = new ArrayList<Integer>();

	public int popEmptyPage() {
		int pageNo = emptyPages.get(0);
		emptyPages.remove(0);
		return pageNo;
	}

	public void addEmptyPage(int pageNo) {
		emptyPages.add(pageNo);
	}

	public void removeEmptyPage(int pageNo) {
		for (int i = 0; i < emptyPages.size(); i++) {
			if (emptyPages.get(i) == pageNo)
				emptyPages.remove(i);
		}
	}

	public int popUnfixedPage() {
		int pageNo = unfixedPages.get(0);
		unfixedPages.remove(0);
		return pageNo;
	}

	public void unfixPage(int pageNo) {
		PageInfo info = bmt.get(pageNo);
		if (info.getFixcount() == 0) {
			unfixedPages.add(pageNo);
		}
	}

	public void addUnfixedPage(int pageNo) {
		unfixedPages.add(pageNo);
	}

	public void removeUnfixedPage(int pageNo) {
		int index = -1;
		for (int i = 0; i < unfixedPages.size(); i++) {
			if (unfixedPages.get(i) == pageNo)
				index = i;
		}
		unfixedPages.remove(index);
	}

	public void setPageLimit(int pageLimit) {
		this.pageLimit = pageLimit;
	}

	public int getPageLimit() {
		return pageLimit;
	}

	public int getPageNum() {
		return pageNum;
	}

	public void setPageNum(int pageNum) {
		this.pageNum = pageNum;
	}

	private int itemSize = DataItem.getItemSize();

	public void setItemSize(int itemSize) {
		this.itemSize = itemSize;
	}

	public void initiateTable(int pageNum) {
		bmt = new ArrayList<PageInfo>();
		for (int i = 0; i < pageNum; i++) {
			PageInfo info = new PageInfo(null, 0, 0, 0);
			bmt.add(info);
		}
	}

	public BufferManager(int pageNum) {
		this.pageNum = pageNum;
		buffer = new byte[512 * pageNum];
		initiateTable(pageNum);
		initBuffer();
		pageBind = new Hashtable<String, Integer>();
	}

	public BufferManager(int bufferSize, boolean setSize) {
		this.pageNum = bufferSize / 512;
		buffer = new byte[512 * pageNum];
		initiateTable(pageNum);
		initBuffer();
		pageBind = new Hashtable<String, Integer>();
	}

	public String makeKey(String filename, int pageNo) {
		return filename + "_" + pageNo;
	}

	/* Write the information of page into hard disk */
	public boolean flushPage(int pageNo) {
		PageInfo info = bmt.get(pageNo);
		String filename = info.getBlockId();
		int pageIndex = info.getPageNo();
		byte[] page = new byte[512];
		int start = pageNo * 512;
		int index = 0;
		for (int i = start; i < start + 512; i++) {
			page[index] = buffer[i];
			index++;
		}
		if(filename == null) {return true;}
		return getFileOperator(filename).writePage(page, pageIndex);
	}

	public boolean flushPage(int pageNo, String filename, int pageIndex) {
		byte[] page = new byte[512];
		int start = pageNo * 512;
		int index = 0;
		for (int i = start; i < start + 512; i++) {
			page[index] = buffer[i];
			index++;
		}
		return getFileOperator(filename).writePage(page, pageIndex);

	}

	/*
	 * Get the page that contains the information, if it is not in the buffer,
	 * load it into buffer
	 */
	public int loadPage(String filename, int pageNo) {
		String key = makeKey(filename, pageNo);
		if (pageBind.containsKey(key)) {
			return pageBind.get(key);
		} else {
			int newPageNo = fetchPage(filename, pageNo);
			if (newPageNo != -1) {
				loadPageFromHD(newPageNo, filename, pageNo);
				pageBind.put(key, newPageNo);
				return newPageNo;
			}
			return -1;// There is no buffer page available
		}
	}

	/* Clear a page in memory */
	public boolean freePage(int pageNo) {
		pointer = pageNo * 512;
		writeInt(0);
		write(new byte[508]);
		return true;
	}

	/* initializing the buffer */
	public void initBuffer() {
		pointer = 0;
		for (int i = 0; i < pageNum; i++) {
			freePage(i);
		}
	}

	/* When the memory do no have the page, we need to fetch an empty page */
	/*
	 * There are several cases: 1. When there is still empty page left, get an
	 * empty page. 2. When no more page is left, flush and clear a page that can
	 * be used 3. When no more page is left, and not even a unfixed page left,
	 * return -1
	 */
	public int fetchPage(String filename, int pageNo) {
		int result = -1;
		if (pageCursor != pageNum) {
			// PageInfo pginfo = bmt.get(pageCursor);
			// pginfo.fix();
			// pginfo.setBlockId(filename);
			// pginfo.setPageNo(pageNo);
			result = pageCursor;
			pageCursor++;
		} else {
			if (emptyPages.size() != 0) {
				int buffPageNo = popEmptyPage();
				// PageInfo pginfo = bmt.get(buffPageNo);
				// pginfo.fix();
				// pginfo.setBlockId(filename);
				// pginfo.setPageNo(pageNo);
				result = buffPageNo;
			} else {
				// Now we have run out of all the empty pages, it is time to
				// swap
				// pages out
				// Get the first unfixed page out, the algorithm is close to LRU
				if (unfixedPages.size() != 0) {
					int buffPageNo = popUnfixedPage();
					PageInfo pginfo = bmt.get(buffPageNo);
					if (pginfo.dirtybit != 0)
						flushPage(buffPageNo);
					freePage(buffPageNo);
					// pginfo.fix();
					// pginfo.setBlockId(filename);
					// pginfo.setPageNo(pageNo);
					result = buffPageNo;
				}
			}
		}
		return result;
	}

	/*
	 * Read the data item, we always try to fetch data from the memory first
	 * However, there still could be cases where all the pages are fixed, in
	 * this case, we will readdirectly from the hard disk
	 */
	public DataItem readItem(String filename, int id) {
//		File f = new File(filename);
//		if(!f.exists()) return null;
		int pageNum = DBUtils.myhash(id, pageLimit);
		boolean isFound = false;
		int total = 0;
		DataItem d = null;
		while (!isFound) {
			total++;
			if (total >= pageLimit)
				return null;
			int newPageNum = loadPage(filename, pageNum);
			if (newPageNum == -1) { // We cannot find an available page from the
									// memory,read from the disk directly
				d = getFileOperator(filename).readItem(id);
			} else {
				d = readResult(newPageNum, id);
				unfixPage(newPageNum);
			}
			if (d == null) {
				isFound = false;
				pageNum = pageNum + 1;
				pageNum = pageNum % pageLimit;
			} else {
				isFound = true;
			}
		}
		return d;
	}

	public boolean writeItem(DataItem d, String filename) {
		boolean success = true;
		int pageNum = DBUtils.myhash(d.getId(), pageLimit);
		boolean isInserted = false;
		int total = 0;
		while (!isInserted) {
			total++;
			if (total > pageLimit)
				return false;
			int newPageNum = loadPage(filename, pageNum);
			if (newPageNum == -1) { // no available pages,write to the hard disk
									// directly
				isInserted = getFileOperator(filename).writeItem(d);
			} else {
				isInserted = writeRecord(d, newPageNum);
				unfixPage(newPageNum);
			}
			pageNum = pageNum + 1;
		}
		return true;
	}
	
	public boolean writeNullItem(int id, String filename) {
		boolean success = true;
		int pageNum = DBUtils.myhash(id, pageLimit);
		boolean isInserted = false;
		int total = 0;
		while (!isInserted) {
			total++;
			if (total > pageLimit)
				return false;
			int newPageNum = loadPage(filename, pageNum);
			if (newPageNum == -1) { // no available pages,write to the hard disk
									// directly
				isInserted = getFileOperator(filename).writeNullItem(id,pageNum);
				if(isInserted == false) return true;
			} else {
				isInserted = writeNullItem(id, newPageNum);
				unfixPage(newPageNum);
			}
			pageNum = pageNum + 1;
		}
		return true;
	}

	public boolean writeNullItem(int id,int pageNum) {
			int pagehead = pageNum * 512;
			seek(pagehead);
			int count = readInt();
			
			int endIndex = 512 * (pageNum + 1) - 4;
			
			for(int i = 0;i<count;i++) {
				seek(endIndex);
				int currentid = readInt();
				if(currentid == id) {
					seek(endIndex);
					writeInt(0);
					return true;
				}
				endIndex = endIndex - 4;
			}
			return false;
	}
	public boolean forceCreateFile(String filename) {
		return getFileOperator(filename).forceCreateFile();
	}
	/*
	 * This is to write the contents in the buffer file back into content
	 */
	public boolean backUpFile(String filename, BeforeImage bfImage) {
		forceCreateFile(filename+".tmp");
		for (int i = 0; i < bmt.size(); i++) {
			PageInfo info = bmt.get(i);
			if (info.getBlockId()!=null&&info.getBlockId().equals(filename)) {
				if (info.dirtybit != 0)
					flushPage(i, filename + ".tmp", info.getPageNo());
				freePage(i);
				bfImage.setPgInfo(info);
				bmt.set(i, new PageInfo());
			}
		}
		return true;
	}

	/*
	 * Update the stuff in the backup folder into buffer
	 */
	public boolean restoreBuffer(String filename) {
		boolean success = true;
		for (int i = 0; i < bmt.size(); i++) {
			PageInfo info = bmt.get(i);
			if (info.getBlockId().equals(filename)) {
				if (info.getDirtybit() != 0)
					flushPage(i);
				freePage(i);
				success = loadPageFromHD(i, filename, info.getPageNo());
			}
		}
		return success;
	}

	public boolean loadPageFromHD(int pageNo, String filename, int index) {
		PageInfo pginfo = bmt.get(pageNo);
		pginfo.setBlockId(filename);
		pginfo.setPageNo(index);
		pginfo.setClean();
		pginfo.setUnFixed();
		byte[] page = getFileOperator(filename).readPage(index);
		seek(pageNo * 512);
		write(page);
		return true;
	}

	public boolean flushAll() {
		boolean success = true;
		for (int i = 0; i < bmt.size(); i++) {
			PageInfo pginfo = bmt.get(i);
			if (pginfo.dirtybit != 0) {
				success = flushPage(i);
			}
		}
		return success;
	}

	/**
	 * ------------------For executing the commands--------------------------
	 */
	public void addFileOperator(String filename) {
		MyFileOperator operator = new MyFileOperator();
		operator.setItemSize(DataItem.getItemSize());
		operator.setMyfilename(filename);
		operator.setPageLimit(pageLimit);
		/*File f = new File(filename);
		if(!f.exists()) operator.createRDF();*/
		operatorList.put(filename, operator);
	}

	public MyFileOperator getFileOperator(String filename) {
		if (operatorList.containsKey(filename)) {
			return operatorList.get(filename);
		} else {
			addFileOperator(filename);
			return operatorList.get(filename);
		}
	}

	public boolean deleteFile(String filename) {
		boolean success = getFileOperator(filename).deleteFile();
		operatorList.remove(filename);
		return success;
	}

	public boolean createNewFile(String filename) {
		return getFileOperator(filename).createNewFile();
	}
	/*
	 * public boolean hasFileOperator(String filename) { return
	 * operatorList.containsKey(filename); }
	 */

	/* This method reads data from the file on harddisk directly */
	public DataItem executeReadHD(MyCommand cmd) {
		if (cmd.isReadCommand()) {
			String filename = cmd.getFilename();
//			File f = new File(filename);
//			if(!f.exists()) return null;
			return getFileOperator(filename).readItem(cmd.getId());
		}
		return null;
	}

	public DataItem executeRead(MyCommand cmd) {
		if (this.getPageNum() == 0) {
			return executeReadHD(cmd);
		} else {
			return readItem(cmd.getFilename(), cmd.getId());
		}
	}

	public DataItem queryItem(String filename, int id) {
		if (this.getPageNum() == 0) {
			return getFileOperator(filename).readItem(id);
		} else {
			return readItem(filename, id);
		}
	}

	public ArrayList<DataItem> matchItem(String filename, String areacode) {
		/*File f = new File(filename);
		if(!f.exists()) return null;*/
		if(this.getPageNum()==0) {
			return getFileOperator(filename).matchItem(areacode);
		} else {
			ArrayList<DataItem> items = new ArrayList<DataItem>();
			for(int i = 0;i<pageLimit;i++) {
				String key = makeKey(filename,i);
				if(pageBind.containsKey(key)) {
					searchPage(pageBind.get(key),items,areacode);
				} else {
					getFileOperator(filename).searchPage(i, items, areacode);
				}
			}
			return items;
		}
	}
	
	public boolean executeUpdateHD(MyCommand cmd) {
		if (cmd.isWriteCommand()) {
			String filename = cmd.getFilename();
			getFileOperator(filename).writeItem(cmd.getDataItem());
		} else if (cmd.isDeleteCommand()) {
			String filename = cmd.getFilename();
			getFileOperator(filename).deleteFile();
		}
		return true;
	}

	public boolean executeUpdate(MyCommand cmd) {
		if (this.getPageNum() == 0) {
			return executeUpdateHD(cmd);
		} else {
			return writeItem(cmd.getDataItem(),cmd.getFilename());
		}
	}

	// Operations on the memory
	// -------------------------Operations on
	// memory------------------------------//
	private ByteBuffer bb;
	private int pointer = 0;

	public void seek(int loc) {
		pointer = loc;
	}

	public int readInt() {
		int steps = 4;
		byte[] b = new byte[steps];
		int index = 0;
		while (index < steps) {
			b[index] = buffer[pointer];
			pointer++;
			index++;
		}
		bb = ByteBuffer.wrap(b);
		return bb.getInt();
	}

	public char readChar() {
		int steps = Character.SIZE;
		byte[] b = new byte[steps];
		int index = 0;
		while (index < steps) {
			b[index] = buffer[pointer];
			pointer++;
			index++;
		}
		bb = ByteBuffer.wrap(b);
		return bb.getChar();
	}

	public void writeInt(int value) {
		int steps = 4;
		int index = 0;
		bb = ByteBuffer.allocate(4);
		bb.putInt(value);
		byte[] src = bb.array();
		while (index < steps) {
			buffer[pointer] = src[index];
			pointer++;
			index++;
		}
	}

	public void write(byte[] src) {
		int steps = src.length;
		int index = 0;
		while (index < steps) {
			buffer[pointer] = src[index];
			pointer++;
			index++;
		}
	}

	public void readFully(byte[] dst) {
		int steps = dst.length;
		int index = 0;
		while (index < steps) {
			dst[index] = buffer[pointer];
			pointer++;
			index++;
		}
	}
	
	public byte readByte() {
		byte b = buffer[pointer];
		pointer++;
		return b;
	}

	private int readPointer(int pageNum, int id) throws IOException {
		int endIndex = 512 * (pageNum + 1) - 4;
		int pageStart = 512 * pageNum;
		seek(pageStart);
		int count = readInt();
		for(int i = 0;i<count;i++) {
			seek(endIndex);
			int currentid = readInt();
			if (currentid == id)
				return i;
			endIndex = endIndex - 4;
		}
		return -1;
	}

	public DataItem readResult(int pageNum, int id) {
		int idx;
		try {
			idx = readPointer(pageNum, id);
			if (idx == -1) {
				return null;
			} else {
				int start = 4 + pageNum * 512 + idx * itemSize;
				seek(start);
				DataItem item = new DataItem();
				item.setId(readInt());
				byte[] clientBytes = new byte[DataItem.clientLength];
				readFully(clientBytes);
				byte[] phoneBytes = new byte[DataItem.phoneLength];
				readFully(phoneBytes);
				item.setClientName(clientBytes);
				item.setPhone(phoneBytes);
				return item;
			}
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
			return null;
		}

	}
    /*
	public boolean writeRecord(DataItem d, int pageNum) {
		bmt.get(pageNum).setDirty();
		int pagehead = pageNum * 512;
		seek(pagehead);
		int count = readInt();
		if (count >= 9) {
			return false;
		}
		int pointerIndex = pagehead + 512 - (count + 1) * 4;
		int recordIndex = pagehead + count * itemSize + 4;
		seek(pointerIndex);
		writeInt(d.getId());
		seek(recordIndex);
		writeInt(d.getId());
		write(d.getClientName());
		write(d.getPhone());
		count = count + 1;
		seek(pagehead);
		writeInt(count);
		return true;

	}*/
	public boolean writeRecord(DataItem d,int pageNum) {
		bmt.get(pageNum).setDirty();
		int pagehead = pageNum * 512;
		seek(pagehead);
		int count = readInt();
		
		int id = d.getId();
		int endIndex = 512 * (pageNum + 1) - 4;
		int start = 4 + 512 * pageNum ;
		
		for(int i = 0;i<count;i++) {
			seek(endIndex);
			int currentid = readInt();
			if(currentid == id) {
				seek(endIndex);
				writeInt(d.getId());
				seek(start);
				writeInt(d.getId());
				write(d.getClientName());
				write(d.getPhone());
				return true;
			}
			endIndex = endIndex - 4;
			start = start + itemSize;
		}
		if(count >= 14) {
			return false;
		} else {
			seek(endIndex);
			writeInt(d.getId());
			seek(start);
			writeInt(d.getId());
			write(d.getClientName());
			write(d.getPhone());
			count ++ ;
			seek(pagehead);
			writeInt(count);
			return true;
		}
	}
	

	public void searchPage(int pageNo, ArrayList<DataItem> items,
			String areacode) {
		int pageStart = pageNo * 512;
		char sa = areacode.charAt(0);
		char sb = areacode.charAt(1);
		char sc = areacode.charAt(2);

		seek(pageStart);
		int count = readInt();
		for (int i = 0; i < count; i++) {
			int areaStart = i * 34 + 26 + pageStart;
			seek(areaStart);
			char a = (char)readByte();
			char b = (char)readByte();
			char c = (char)readByte();
			if ((a == sa) && (b == sb) && (c == sc)) {
				int recordStart = 4 + pageStart + i * itemSize;
				seek(recordStart);
				DataItem item = new DataItem();
				item.setId(readInt());
				byte[] clientBytes = new byte[DataItem.clientLength];
				readFully(clientBytes);
				byte[] phoneBytes = new byte[DataItem.phoneLength];
				readFully(phoneBytes);
				item.setClientName(clientBytes);
				item.setPhone(phoneBytes);
				items.add(item);
			}

		}

	}
	// -----------------------To here: the operations are on the virtual
	// buffer--------------//
}
